ECMAScript 双月报告:Hashbang Grammer 提案成功进入到 Stage 4
作者 @穹心
审校 @昭朗
本次会议中,Hashbang Grammer 提案成功进入到 Stage 4,将在 ECMAScript 2023 中被作为正式语言特性加入到 JavaScript 当中。在上一次会议中获得了阶段性突破的 Duplicate named capturing groups 与 Import Reflection 提案,在本次会议中也再次实现了 Stage 的推进。除此以外,还有 Function Memoization 、Object.pick/omit 等在本次会议中首次推进到 Stage 1 的提案。
Stage 3 → Stage 4
从 Stage 3 进入到 Stage 4 有以下几个门槛:
必须编写与所有提案内容对应的 tc39/test262[1]测试,用于给各大 JavaScript 引擎和 transpiler 等实现检查与标准的兼容程度,并且 test262 已经合入了提案所需要的测试用例; 至少要有两个实现能够兼容上述 Test 262 测试,并发布到正式版本中; 发起了将提案内容合入正式标准文本tc39/ecma262[2]的 Pull Request,并被 ECMAScript 编辑签署同意意见。
Hashbang Grammar
提案链接:proposal-hashbang[3]
Hashbang (也称 Shebang)语法常用于在类 Unix 系统下指定此脚本文件的解释器,它的语法大致是这样:
#!/usr/bin/env node
console.log("ecma");
JavaScript 作为一门解释性语言,其源码需要运行时将其解释为机器码才能运行,举例来说,使用 node :
$ node index.js
这一命令其实就指明了,我们在使用 node 来解释执行 index.js 文件。而“使用 node”这一信息,其实就可以通过上面的 Shebang 来将其内联到文件中,然后我们就可以直接运行此文件(需要chmod +x index.js
):
$ ./index.js
/usr/bin/env
实际上是一个可执行程序,它将基于后面的参数为我们寻找实际程序,即 /usr/bin/env node
将指向操作系统上的 node 路径,这样我们就不需要自己写死 node 的安装路径了。
而此提案的主要作用在于,此前解释器所获得的 JS 代码是已经去除了 Shebang 的部分,而此提案会将 Shebang 的代码也完整地传递给引擎,由引擎层面来进行统一的标准化处理。
Stage 2 → Stage 3
提案从 Stage 2 进入到 Stage 3 有以下几个门槛:
撰写了包含提案所有内容的标准文本,并有指定的 TC39 成员审阅并签署了同意意见; ECMAScript 编辑签署了同意意见。
Duplicate named capturing groups
提案链接:proposal-duplicate-named-capturing-groups[4]
在正则表达式中,我们可以使用捕获组(Capturing Group)来对匹配模式中的某一部分做独立的匹配,如 es+
会匹配 essss
与 esssss
(+
代表匹配一次或更多),而使用匹配组,我们可以将 es
作为一个匹配部分,如 (es)+
会匹配 es
以及 eseses
等。
我们也可以对捕获组进行命名,如 ?<name>
这样的形式,常见的一个场景是结合 str.match
方法:
const dateRegexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/;
const str = "2022-06-01";
const groups = str.match(dateRegexp).groups;
groups.year; // 2022
groups.month; // 06
groups.day; // 01
无法使用同名捕获组匹配一组联合模式,如日期格式还可能是 06-01-2022,我们希望能这么使用联合模式:
const dateRegexp = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})|(?<day>[0-9]{2})-(?<month>[0-9]{2})-(?<year>[0-9]{4})/;
但由于捕获组的命名唯一约束,上面这个表达式是不合法的。
为了解决这一问题,此提案提出允许捕获组的命名不唯一,以此来支持如上面在联合模式中使用捕获组的场景。
Stage 1 → Stage 2
从 Stage 1 进入到 Stage 2 需要完成撰写包含提案所有内容的标准文本的初稿。
Import Reflection
提案链接:proposal-import-reflection[5]
Import Reflection 提案为 import 语句支持了在默认导入名前新增反射类型,来声明导入反射属性(元数据)的能力,其目前语法大致如下:
import module x from "<specifier>";
const x = await import("<specifier>", { reflect: "module" });
这里的 module 即为其反射类型。这一标注会改变 import 语句的对于目标模块的执行方式,以此提案的主要驱动场景之一为例, 为 WebAssembly 模块指定额外的类型,如实例导入(WebAssembly.Instance
)与模块导入(WebAssembly.Module
):
import module FooModule from "./foo.wasm";
FooModule instanceof WebAssembly.Module; // true
// WASI 是适用于 WebAssembly 的模块化系统调用规范
import { WASI } from 'wasi';
const wasi = new WASI({ args, env, preopens });
const fooInstance = await WebAssembly.instantiate(FooModule, {
wasi_snapshot_preview1: wasi.wasiImport
});
wasi.start(fooInstance);
Stage 0 → Stage 1
从 Stage 0 进入到 Stage 1 有以下门槛:
找到一个 TC39 成员作为 champion 负责这个提案的演进; 明确提案需要解决的问题与需求和大致的解决方案; 有问题、解决方案的例子; 对 API 形式、关键算法、语义、实现风险等有讨论、分析。Stage 1 的提案会有可预见的比较大的改动,以下列出的例子并不代表提案最终会是例子中的语法、语义。
Symbol Predicates
提案链接:proposal-symbol-predicates[6]
此提案为 Symbol 顶级对象引入了两个新的方法:Symbol.isRegistered
与 Symbol.isWellKnown
,它们分别用于判断一个 Symbol 值是否已被注册,以及是否是 ECMA262 & ECMA402 规范中内置的 Symbol 类型(如 Symbol.iterator
、Symbol.toPrimitive
等)。
这个提案主要是为了解决在 Symbol as WeakMap Key 提案中,仅有 Unique Symbol(直接通过 Symbol()
创建的 Symbol 值) 与 Well-known Symbol(内置 Symbol) 可以作为 WeakMap 结构 key 的问题。
你也可以使用这两个方法来判断一个 Symbol 类型是否是独一无二的:
const isUniqueSymbol = sym => typeof sym === "symbol" && !(Symbol.isRegistered(sym) || Symbol.isWellKnown(sym));
isUniqueSymbol(Symbol()); // true 一个新的 Symbol 类型
isUniqueSymbol(Symbol.for("foo")); // false Symbol.for 方法会将此 Symbol 注册到全局
isUniqueSymbol(Symbol.asyncIterator); // false 内置 Symbol 类型
isUniqueSymbol({}); // false 非 Symbol 类型
Policy Maps and Sets
提案链接:proposal-policy-map-set[7]
缓存在编程实践中一直是一个重要的领域,前端开发者和它打交道的次数更是数不胜数:DNS缓存、HTTP缓存、CDN缓存、本地缓存、服务器缓存等等。在 npm 社区,你也能找到许多用于缓存设计的工具包,如基于 LRU 策略的 lru-cache[8]与 quick-lru[9]等。
此提案尝试为 JavaScript 中引入原生的缓存策略实现,包括 LRU (Least Recently Used,最近最少使用)、LFU(Least Frequently Used,最不常用)、FIFO(First In First Out,先进先出)与 LIFO (Last In First Out,后进先出),它们被实现为内置数据结构的形式:
new FIFOMap(maxNumOfEntries, entries = [])
new FIFOSet(maxNumOfValues, values = [])
new LIFOMap(maxNumOfEntries, entries = [])
new LIFOSet(maxNumOfValues, values = [])
new LRUMap(maxNumOfEntries, entries = [])
new LRUSet(maxNumOfValues, values = [])
new LFUMap(maxNumOfEntries, entries = [])
new LFUSet(maxNumOfValues, values = [])
这些结构基本实现了 Map 与 Set 上的方法(但它们并不是 Map 与 Set 的子类型),你也可以通过这些构造函数的 maxNumOfEntries / maxNumOfValues 来控制这些缓存结构的可用内存。
Function Memoization
提案链接:proposal-function-memo[10]
函数缓存指的是,对于一个函数建立起入参-结果的缓存表,在函数被使用某一新的入参调用时的返回值缓存起来,并在后续再次使用这一入参时直接返回此缓存值,而不会实际调用函数逻辑。
对于存在较大开销的计算过程,以及从状态到 UI 组件的计算这种场景,函数缓存会是非常好的优化手段,同时也可以基于其更好地实现单例模式(如确保对对象返回的是同一个引用)。
目前此提案提出的方式是新增 Function.prototype.memo
方法,也就是说对一个函数调用 memo 方法后,将返回它的缓存版本:
function f (x) { console.log(x); return x * 2; }
const fMemo = f.memo();
fMemo(3); // 打印 3,返回 6
fMemo(3); // 直接返回 6
fMemo(2); // 打印 2,返回 4
fMemo(2); // 直接返回 4
fMemo(3); // 直接返回 6
为了更简单地获取函数的缓存版本,此提案提出同时新增 @Function.memo
装饰器,来直接将一个函数标记为缓存版本(将无法再访问原版本):
@Function.memo
function f (x) { console.log(x); return x * 2; }
另外,此提案也希望将缓存表的控制也暴露出去,也就是说你可以自己传入一个实现了 .get()
.has()
.set()
.get()
方法的类 Map 结构,来作为函数的缓存控制,上面提到的 Policy Maps and Sets 提案在这里就大有可为。
Object pick/omit
提案链接:proposal-object-pick-or-omit[11]
此提案将引入两个 Object 对象上的顶级方法:Object.pick 与 Object.omit,它们的作用正如其名,pick 将提取对象中的特定部分,而 omit 将移除对象中的特定部分。如果你使用过 Lodash 的 pick 和 omit 方法,那么应该对这两种操作非常熟悉。
目前在 JavaScript 中,我们可以通过解构赋值的方式来实现类 omit 的操作:
// 移除 obj 的 name、age 属性后得到 rest
const { name, age, ...rest } = obj;
但问题在于,如果我们想要移除的键名是动态的,那么这一方式就完全失效了,同时也无法基于解构赋值实现类 pick 的操作(pick 应当是基于子集进行处理,而非反过来基于差集)。另外,解构赋值并不能对原型对象上的属性进行处理。
使用这两个方法,我们可以进行更加符合直觉的对象操作了:
Object.pick(obj, ['job', 'sex']);
Object.omit(obj, ['name', 'age']);
除了基于键名来进行操作,这两个方法也支持使用一个 predictedFunction 函数来进行基于键值的判断,在此条件中返回 true 的属性将对应的被保留/移除:
Object.pick({a : 1, b : 2}, v => v === 1); // => { a: 1 }
这一使用方式类似于 Lodash 中的 pickBy / omitBy 方法。
结语
由贺师俊牵头,阿里巴巴前端标准化小组等多方参与组建的 JavaScript 中文兴趣小组(JSCIG,JavaScript Chinese Interest Group)在 GitHub 上开放讨论各种 ECMAScript 的问题,非常欢迎有兴趣的同学参与讨论:https://github.com/JSCIG/es-discuss/discussions 。
参考资料
tc39/test262: https://github.com/tc39/test262
[2]tc39/ecma262: https://github.com/tc39/ecma262
[3]proposal-hashbang: https://github.com/tc39/proposal-hashbang
[4]proposal-duplicate-named-capturing-groups: https://github.com/tc39/proposal-duplicate-named-capturing-groups
[5]proposal-import-reflection: https://github.com/tc39/proposal-import-reflection
[6]proposal-symbol-predicates: https://github.com/rricard/proposal-symbol-predicates
[7]proposal-policy-map-set: https://github.com/tc39/proposal-policy-map-set
[8]lru-cache: https://www.npmjs.com/package/lru-cache
[9]quick-lru: https://www.npmjs.com/package/quick-lru
[10]proposal-function-memo: https://github.com/js-choi/proposal-function-memo
[11]proposal-object-pick-or-omit: https://github.com/tc39/proposal-object-pick-or-omit